package senzuri.acceleraudio.ui;

import java.io.*;
import java.util.Random;

import senzuri.acceleraudio.R;
import senzuri.acceleraudio.data.Database;
import senzuri.acceleraudio.util.InfiniteBuffer;
import senzuri.acceleraudio.util.MicrophoneReader;
import senzuri.acceleraudio.util.MicrophoneReader.MicrophoneListener;
import android.app.*;
import android.content.*;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.hardware.*;
import android.location.*;
import android.os.*;
import android.support.v4.app.NotificationCompat;

public class RecordService extends Service implements SensorEventListener, LocationListener, MicrophoneListener {
  private static final int ONGOING_NOTIFICATION_ID = 0x5487;
  private static final int AUDIO_FREQUENCY = 8000;
  private static RecordService instance;
  
  private final IBinder binder = new LocalBinder();
  private SensorManager sensorManager;
  private Sensor accelerometer;
  private LocationManager locationManager;
  private MicrophoneReader microphoneReader;
  private OnUpdateListener listener;
  
  private InfiniteBuffer.Float accelerometerValues0;
  private InfiniteBuffer.Float accelerometerValues1;
  private InfiniteBuffer.Float accelerometerValues2;
  private InfiniteBuffer.Long accelerometerValuesTimestamps;
  
  private InfiniteBuffer.Double locationValues0;
  private InfiniteBuffer.Double locationValues1;
  private InfiniteBuffer.Double locationValues2;
  private InfiniteBuffer.Long locationValuesTimestamps;
  
  private InfiniteBuffer.Short microphoneValues;
  
  private int maxLength;
  private long pauseStart, totalPauseTime;
  private State state;
  private Notification notification;
  private Location lastLocation;
  private SensorEvent lastSensorEvent;

  @Override public int onStartCommand(Intent intent, int flags, int startId) {
    init();
    return START_STICKY;
  }

  @Override public IBinder onBind(Intent intent) {
    init();
    return binder;
  }

  private void init() {
    if (instance == this)
      return;
    if (instance != null)
      throw new IllegalStateException("Only one instance of RecordService is allowed!");
    instance = this;

    Intent notificationIntent = new Intent(this, RecordActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
    notification = new NotificationCompat.Builder(this)
        .setContentTitle(getText(R.string.app_name))
        .setContentText(getText(R.string.recording))
        .setSmallIcon(R.drawable.ic_launcher)
        .setWhen(System.currentTimeMillis())
        .setContentIntent(pendingIntent)
        .build();
    
    sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
    accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
    locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
    microphoneReader = new MicrophoneReader(this, AUDIO_FREQUENCY);

    accelerometerValues0 = new InfiniteBuffer.Float(256);
    accelerometerValues1 = new InfiniteBuffer.Float(256);
    accelerometerValues2 = new InfiniteBuffer.Float(256);
    accelerometerValuesTimestamps = new InfiniteBuffer.Long(256);
    
    locationValues0 = new InfiniteBuffer.Double(256);
    locationValues1 = new InfiniteBuffer.Double(256);
    locationValues2 = new InfiniteBuffer.Double(256);
    locationValuesTimestamps = new InfiniteBuffer.Long(256);
    
    microphoneValues = new InfiniteBuffer.Short(8000);
    
    state = State.paused;
  }

  @Override public void onDestroy() {
    instance = null;
  }
  
/*****************************************************************************/

  @Override public void onSensorChanged(SensorEvent event) {
    this.lastSensorEvent = event;
    
    if (state != State.recording)
      return;
    
    if (event.sensor == accelerometer) {
      try {
        accelerometerValuesTimestamps.add(System.nanoTime() - totalPauseTime);
        accelerometerValues0.add(event.values[0]);
        accelerometerValues1.add(event.values[1]);
        accelerometerValues2.add(event.values[2]);
      } catch (Exception e) {
        // Se ci sono problemi con la scrittura, crasha
        throw new RuntimeException(e);
      }

      if (listener != null)
        listener.onUpdateAccelerometer(event.values[0], event.values[1], event.values[2]);
    }
  }

  @Override public void onLocationChanged(Location location) {
    this.lastLocation = location;
    
    if (state != State.recording)
      return;
    
    try {
      locationValuesTimestamps.add(System.nanoTime() - totalPauseTime);
      locationValues0.add(location.getLatitude());
      locationValues1.add(location.getLongitude());
      locationValues2.add(location.getAltitude());
    } catch (Exception e) {
      // Se ci sono problemi con la scrittura, crasha
      throw new RuntimeException(e);
    }

    if (listener != null)
      listener.onUpdateLocation(location.getLatitude(), location.getLongitude(), location.getAltitude());
  }

  @Override public void onMicrophoneData(short[] samples, int count) {
    if (state != State.recording)
      return;
    
    int sum = 0;
    for (short s : samples)
      sum += Math.abs(s);
    int avg = sum / samples.length;
    
    try {
      microphoneValues.add(samples, 0, count);
    } catch (Exception e) {
      // Se ci sono problemi con la scrittura, crasha
      throw new RuntimeException(e);
    }

    if (listener != null)
      listener.onUpdateMicrophone(avg);
    
    if (microphoneValues.size() >= maxLength)
      finishRecording();
  }
  
/*****************************************************************************/

  public void setOnUpdateListener(OnUpdateListener listener) {
    this.listener = listener;
  }
  
  public boolean isRecording() {
    return state == State.recording;
  }

  public boolean hasRecorded() {
    return totalPauseTime != 0;
  }
  
  public void startRecording() {
    if (state != State.paused)
      throw new IllegalStateException();

    SharedPreferences preferences = getSharedPreferences("", Context.MODE_PRIVATE);
    maxLength = preferences.getInt("recordLength", 3) * 60 * 8000;

    startForeground(ONGOING_NOTIFICATION_ID, notification);
    long now = System.nanoTime();
    totalPauseTime += now - pauseStart;
    
    sensorManager.registerListener(this, accelerometer,
        preferences.getInt("accelerometerRate", SensorManager.SENSOR_DELAY_NORMAL));
    locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
    locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this);
    
    microphoneReader.setListener(this);
    
    state = State.recording;
  }
  
  public void pauseRecording() {
    if (state != State.recording)
      throw new IllegalStateException();

    sensorManager.unregisterListener(this, accelerometer);
    locationManager.removeUpdates(this);
    microphoneReader.setListener(null);
    
    pauseStart = System.nanoTime();
    state = State.paused;
  }
  
  public void finishRecording() {
    if (state == State.recording)
      pauseRecording();
    
    stopSelf();
    state = State.finished;
    stopForeground(true);
    
    this.listener.onFinishRecording();
  }

  public int getAccelerometerSamples() {
    return accelerometerValuesTimestamps.size();
  }

  public int getMicrophoneSamples() {
    return microphoneValues.size();
  }

  public int getLocationSamples() {
    return locationValuesTimestamps.size();
  }

  public Location getLocation() {
    return lastLocation;
  }

  public SensorEvent getForce() {
    return lastSensorEvent;
  }

  public void saveData(int data) throws IOException {
    if (state != State.finished)
      throw new IllegalStateException();

    File accDataFile = Database.getInstance(this).createFile(data + ".acc");
    File locDataFile = Database.getInstance(this).createFile(data + ".loc");
    File micDataFile = Database.getInstance(this).createFile(data + ".pcm");
    File thumbnailFile = Database.getInstance(this).createFile(data + ".png");
    
    if (accDataFile == null || thumbnailFile == null || micDataFile == null || locDataFile == null)
      throw new IOException("Couldn't create files");
    
    DataOutputStream out;
    
    // Salva i dati dal microfono
    out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(micDataFile)));
    for (int i = 0; i < microphoneValues.size(); i++)
      out.writeShort(microphoneValues.get(i));
    out.close();
    
    // Salva i dati dal accelerometro
    out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(accDataFile)));
    for (int i = 0; i < accelerometerValuesTimestamps.size(); i++) {
      out.writeLong(accelerometerValuesTimestamps.get(i));
      out.writeFloat(accelerometerValues0.get(i));
      out.writeFloat(accelerometerValues1.get(i));
      out.writeFloat(accelerometerValues2.get(i));
    }
    out.close();

    // Salva i dati dal gps
    out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(locDataFile)));
    for (int i = 0; i < locationValuesTimestamps.size(); i++) {
      out.writeLong(locationValuesTimestamps.get(i));
      out.writeDouble(locationValues0.get(i));
      out.writeDouble(locationValues1.get(i));
      out.writeDouble(locationValues2.get(i));
    }
    out.close();

    // Salva l'icona
    Bitmap bi = Bitmap.createBitmap(512, 512, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bi);
    Paint paint = new Paint();
    Random rnd = new Random(
          microphoneValues.size()
          + ((long)accelerometerValuesTimestamps.size() << 32)
          + ((long)locationValuesTimestamps.size() << 56)
        );
    paint.setColor(0xff000000 | rnd.nextInt());
    canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), paint);
    int shapes = 1 + rnd.nextInt(15);
    for (int i = 0; i < shapes; i++) {
      int x = rnd.nextInt(canvas.getWidth());
      int y = rnd.nextInt(canvas.getHeight());
      int w = rnd.nextInt(canvas.getWidth() - x);
      int h = rnd.nextInt(canvas.getHeight() - y);
      char[] string = new char[rnd.nextInt(10) + 1];
      for (int j = 0; j < string.length; j++)
        string[j] = (char)rnd.nextInt();
      
      RectF rect = new RectF(x, y, x + w, y + h);
      
      paint.setColor(0xff000000 | rnd.nextInt());
      paint.setTextSize(22 + rnd.nextInt(20));
      
      switch (rnd.nextInt(4)) {
      case 0:
        canvas.drawRect(rect, paint);
        break;
      case 1:
        canvas.drawOval(rect, paint);
        break;
      default:
        canvas.drawText(string, 0, string.length, x, y, paint);
        break;
      }
    }
    
    FileOutputStream thumbnailStream = new FileOutputStream(thumbnailFile);
    bi.compress(Bitmap.CompressFormat.PNG, 100, thumbnailStream);
    thumbnailStream.close();
  }
  
/*****************************************************************************/

  // Classi d'appoggio per fare le cose
  
  private enum State {
    paused, recording, finished
  }
  
  public class LocalBinder extends Binder {
    RecordService getService() {
      return RecordService.this;
    }
  }

  public interface OnUpdateListener {
    public void onUpdateAccelerometer(float x, float y, float z);
    public void onUpdateLocation(double lat, double lon, double h);
    public void onUpdateMicrophone(int avg);
    public void onFinishRecording();
  }
  
/*****************************************************************************/

  // Tutti gli eventi ignorati:

  @Override public void onAccuracyChanged(Sensor sensor, int accuracy) {}

  @Override public void onStatusChanged(String provider, int status, Bundle extras) {}

  @Override public void onProviderEnabled(String provider) {}

  @Override public void onProviderDisabled(String provider) {}
}
